1 /** 2 Copyright: Copyright (c) 2016-2021, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 10 Utility functions for Clang Compilation Databases. 11 12 # Usage 13 Call the function `fromArgCompileDb` to create one, merged database. 14 15 Extract flags the flags for a file by calling `appendOrError`. 16 17 Example: 18 --- 19 auto dbs = fromArgCompileDb(["foo.json]); 20 auto flags = dbs.appendOrError(dbs, null, "foo.cpp", defaultCompilerFlagFilter); 21 --- 22 */ 23 module compile_db; 24 25 import logger = std.experimental.logger; 26 import std.algorithm : map, filter, splitter, joiner; 27 import std.array : empty, array, appender; 28 import std.exception : collectException; 29 import std.json : JSONValue; 30 import std.path : buildPath; 31 import std.typecons : Nullable; 32 33 import my.path : AbsolutePath, Path; 34 35 public import compile_db.user_filerange; 36 public import compile_db.system_compiler : deduceSystemIncludes, SystemIncludePath, Compiler; 37 38 package void shouldEqual(T0, T1)(T0 a, T1 b) { 39 import std.stdio : writeln; 40 41 if (a != b) { 42 writeln(a, " != ", b); 43 assert(0); 44 } 45 } 46 47 package void shouldBeIn(T0, T1)(T0 a, T1 b) { 48 import std.stdio : writeln; 49 50 bool found; 51 foreach (v; b) { 52 if (a == v) { 53 found = true; 54 break; 55 } 56 } 57 58 if (!found) { 59 writeln(a, " not found in ", b); 60 assert(0); 61 } 62 } 63 64 @safe: 65 66 /** Hold an entry from the compilation database. 67 * 68 * The following information is from the official specification. 69 * $(LINK2 http://clang.llvm.org/docs/JSONCompilationDatabase.html, Standard) 70 * 71 * directory: The working directory of the compilation. All paths specified in 72 * the command or file fields must be either absolute or relative to this 73 * directory. 74 * 75 * file: The main translation unit source processed by this compilation step. 76 * This is used by tools as the key into the compilation database. There can be 77 * multiple command objects for the same file, for example if the same source 78 * file is compiled with different configurations. 79 * 80 * command: The compile command executed. After JSON unescaping, this must be a 81 * valid command to rerun the exact compilation step for the translation unit 82 * in the environment the build system uses. Parameters use shell quoting and 83 * shell escaping of quotes, with ‘"‘ and ‘\‘ being the only special 84 * characters. Shell expansion is not supported. 85 * 86 * argumets: The compile command executed as list of strings. Either arguments 87 * or command is required. 88 * 89 * output: The name of the output created by this compilation step. This field 90 * is optional. It can be used to distinguish different processing modes of the 91 * same input file. 92 * 93 * Additions. 94 * The standard do not specify how to treat "directory" when it is a relative 95 * path. The logic chosen in dextool is to treat it as relative to the path 96 * the compilation database file is read from. 97 */ 98 struct CompileCommand { 99 /// The raw command from the tuples "command" or "arguments value. 100 static struct Command { 101 string[] payload; 102 alias payload this; 103 bool hasValue() @safe pure nothrow const @nogc { 104 return payload.length != 0; 105 } 106 } 107 108 /// File that where compiled. 109 Path file; 110 /// ditto. 111 AbsolutePath absoluteFile; 112 /// Working directory of the command that compiled the input. 113 AbsolutePath directory; 114 /// The executing command when compiling. 115 Command command; 116 /// The resulting object file. 117 Path output; 118 /// ditto. 119 AbsolutePath absoluteOutput; 120 } 121 122 /// The path to the compilation database. 123 struct CompileDbFile { 124 Path payload; 125 alias payload this; 126 127 this(string p) @safe nothrow { 128 payload = Path(p); 129 } 130 } 131 132 /// The absolute path to the directory the compilation database reside at. 133 struct AbsoluteCompileDbDirectory { 134 AbsolutePath payload; 135 alias payload this; 136 137 this(Path path) { 138 import std.path : dirName; 139 140 payload = AbsolutePath(path.dirName.Path); 141 } 142 } 143 144 /// A complete compilation database. 145 struct CompileCommandDB { 146 CompileCommand[] payload; 147 alias payload this; 148 149 bool empty() @safe pure nothrow const @nogc { 150 return payload.empty; 151 } 152 } 153 154 // The result of searching for a file in a compilation DB. 155 // The file may be occur more than one time therefor an array. 156 struct CompileCommandSearch { 157 CompileCommand[] payload; 158 alias payload this; 159 160 bool empty() @safe pure nothrow const @nogc { 161 return payload.empty; 162 } 163 } 164 165 /** 166 * Trusted: opIndex for JSONValue is @safe in DMD-2.077.0 167 * remove the trusted attribute when the minimal requirement is upgraded. 168 */ 169 private Nullable!CompileCommand toCompileCommand(JSONValue v, AbsoluteCompileDbDirectory db_dir) nothrow @trusted { 170 import std.exception : assumeUnique; 171 import std.range : only; 172 import std.utf : byUTF; 173 174 static if (__VERSION__ < 2085L) { 175 import std.json : JSON_TYPE; 176 177 alias JSONType = JSON_TYPE; 178 alias JSONType_array = JSON_TYPE.ARRAY; 179 alias JSONType_string = JSON_TYPE.STRING; 180 } else { 181 import std.json : JSONType; 182 183 alias JSONType_array = JSONType.array; 184 alias JSONType_string = JSONType..string; 185 } 186 187 string[] command = () { 188 string[] cmd; 189 try { 190 cmd = v["command"].str.splitter.filter!(a => a.length != 0).array; 191 } catch (Exception ex) { 192 } 193 194 // prefer command over arguments if both are present because of bugs in 195 // tools that produce compile_commands.json. 196 if (cmd.length != 0) 197 return cmd; 198 199 try { 200 enum j_arg = "arguments"; 201 const auto j_type = v[j_arg].type; 202 if (j_type == JSONType_string) 203 cmd = v[j_arg].str.splitter.filter!(a => a.length != 0).array; 204 else if (j_type == JSONType_array) { 205 import std.range; 206 207 cmd = v[j_arg].arrayNoRef 208 .filter!(a => a.type == JSONType_string) 209 .map!(a => a.str) 210 .filter!(a => a.length != 0) 211 .array; 212 } 213 } catch (Exception ex) { 214 } 215 216 return cmd; 217 }(); 218 219 if (command.length == 0) { 220 logger.error("Unable to parse the JSON tuple. Both command and arguments are empty") 221 .collectException; 222 return typeof(return)(); 223 } 224 225 string output; 226 try { 227 output = v["output"].str; 228 } catch (Exception ex) { 229 } 230 231 try { 232 const directory = v["directory"]; 233 const file = v["file"]; 234 235 foreach (a; only(directory, file).map!(a => !a.isNull && a.type == JSONType_string) 236 .filter!(a => !a)) { 237 // sanity check. 238 // if any element is false then break early. 239 return typeof(return)(); 240 } 241 242 return toCompileCommand(directory.str, file.str, command, db_dir, output); 243 } catch (Exception e) { 244 logger.info("Input JSON: ", v.toPrettyString).collectException; 245 logger.error("Unable to parse json: ", e.msg).collectException; 246 } 247 248 return typeof(return)(); 249 } 250 251 /** Transform a json entry to a CompileCommand. 252 * 253 * This function is under no circumstances meant to be exposed outside this module. 254 * The API is badly designed for common use because it relies on the position 255 * order of the strings for their meaning. 256 */ 257 Nullable!CompileCommand toCompileCommand(string directory, string file, 258 string[] command, AbsoluteCompileDbDirectory db_dir, string output) nothrow { 259 // expects that v is a tuple of 3 json values with the keys directory, 260 // command, file 261 262 Nullable!CompileCommand rval; 263 264 try { 265 auto abs_workdir = AbsolutePath(buildPath(db_dir, directory.Path)); 266 auto abs_file = AbsolutePath(buildPath(abs_workdir, file.Path)); 267 auto abs_output = AbsolutePath(buildPath(abs_workdir, output.Path)); 268 // dfmt off 269 rval = CompileCommand( 270 Path(file), 271 abs_file, 272 abs_workdir, 273 CompileCommand.Command(command), 274 Path(output), 275 abs_output); 276 // dfmt on 277 } catch (Exception ex) { 278 logger.error("Unable to parse json: ", ex.msg).collectException; 279 } 280 281 return rval; 282 } 283 284 /** Parse a CompilationDatabase. 285 * 286 * Params: 287 * raw_input = the content of the CompilationDatabase. 288 * db = path to the compilation database file. 289 * out_range = range to write the output to. 290 */ 291 private void parseCommands(T)(string raw_input, CompileDbFile db, ref T out_range) nothrow { 292 import std.json : parseJSON, JSONException; 293 294 static void put(T)(JSONValue v, AbsoluteCompileDbDirectory dbdir, ref T out_range) nothrow { 295 296 try { 297 // dfmt off 298 foreach (e; v.array() 299 // map the JSON tuples to D structs 300 .map!(a => toCompileCommand(a, dbdir)) 301 // remove invalid 302 .filter!(a => !a.isNull) 303 .map!(a => a.get)) { 304 out_range.put(e); 305 } 306 // dfmt on 307 } catch (Exception ex) { 308 logger.error("Unable to parse json:", ex.msg).collectException; 309 } 310 } 311 312 try { 313 // trusted: is@safe in DMD-2.077.0 314 // remove the trusted attribute when the minimal requirement is upgraded. 315 auto json = () @trusted { return parseJSON(raw_input); }(); 316 auto as_dir = AbsoluteCompileDbDirectory(db.AbsolutePath); 317 318 // trusted: this function is private so the only user of it is this module. 319 // the only problem would be in the out_range. It is assumed that the 320 // out_range takes care of the validation and other security aspects. 321 () @trusted { put(json, as_dir, out_range); }(); 322 } catch (Exception ex) { 323 logger.error("Error while parsing compilation database: " ~ ex.msg).collectException; 324 } 325 } 326 327 void fromFile(T)(CompileDbFile filename, ref T app) { 328 import std.file : readText; 329 330 auto raw = readText(filename); 331 if (raw.length == 0) 332 logger.warning("File is empty: ", filename); 333 334 raw.parseCommands(filename, app); 335 } 336 337 void fromFiles(T)(CompileDbFile[] fnames, ref T app) { 338 import std.file : exists; 339 340 foreach (f; fnames) { 341 if (!exists(f)) 342 throw new Exception("File do not exist: " ~ f); 343 f.fromFile(app); 344 } 345 } 346 347 /** Return default path if argument is null. 348 */ 349 CompileDbFile[] orDefaultDb(string[] cli_path) @safe nothrow { 350 if (cli_path.length == 0) { 351 return [CompileDbFile("compile_commands.json")]; 352 } 353 354 return cli_path.map!(a => CompileDbFile(a)).array(); 355 } 356 357 /** Find a best matching compile_command in the database against the path 358 * pattern `glob`. 359 * 360 * When searching for the compile command for a file, the compilation db can 361 * return several commands, as the file may have been compiled with different 362 * options in different parts of the project. 363 * 364 * Params: 365 * glob = glob pattern to find a matching file in the DB against 366 */ 367 CompileCommandSearch find(CompileCommandDB db, string glob) @safe { 368 foreach (a; db.filter!(a => isMatch(a, glob))) { 369 return CompileCommandSearch([a]); 370 } 371 return CompileCommandSearch.init; 372 } 373 374 /** Check if `glob` fuzzy matches `a`. 375 */ 376 bool isMatch(CompileCommand a, string glob) { 377 import std.path : globMatch; 378 379 if (a.absoluteFile == glob) 380 return true; 381 else if (a.absoluteFile == AbsolutePath(glob)) 382 return true; 383 else if (a.file == glob) 384 return true; 385 else if (globMatch(a.absoluteFile, glob)) 386 return true; 387 else if (a.absoluteOutput == glob) 388 return true; 389 else if (a.output == glob) 390 return true; 391 else if (globMatch(a.absoluteOutput, glob)) 392 return true; 393 return false; 394 } 395 396 string toString(CompileCommand[] db) @safe pure { 397 import std.conv : text; 398 import std.format : formattedWrite; 399 400 auto app = appender!string(); 401 402 foreach (a; db) { 403 formattedWrite(app, "%s\n %s\n %s\n", a.directory, a.file, a.absoluteFile); 404 405 if (!a.output.empty) { 406 formattedWrite(app, " %s\n", a.output); 407 formattedWrite(app, " %s\n", a.absoluteOutput); 408 } 409 410 if (!a.command.empty) 411 formattedWrite(app, " %-(%s %)\n", a.command); 412 } 413 414 return app.data; 415 } 416 417 string toString(CompileCommandDB db) @safe pure { 418 return toString(db.payload); 419 } 420 421 string toString(CompileCommandSearch search) @safe pure { 422 return toString(search.payload); 423 } 424 425 CompileCommandFilter defaultCompilerFilter() { 426 return CompileCommandFilter(defaultCompilerFlagFilter, 0); 427 } 428 429 /// Returns: array of default flags to exclude. 430 auto defaultCompilerFlagFilter() @safe { 431 auto app = appender!(FilterClangFlag[])(); 432 433 // dfmt off 434 foreach (f; [ 435 // remove basic compile flag irrelevant for AST generation 436 "-c", "-o", 437 // machine dependent flags 438 "-m", 439 // machine dependent flags, AVR 440 "-nodevicelib", "-Waddr-space-convert", 441 // machine dependent flags, VxWorks 442 "-non-static", "-Bstatic", "-Bdynamic", "-Xbind-lazy", "-Xbind-now", 443 // blacklist all -f because most aren not compatible with clang 444 "-f", 445 // linker flags, irrelevant for the AST 446 "-static", "-shared", "-rdynamic", "-s", "-l", "-L", "-z", "-u", "-T", "-Xlinker", 447 // a linker flag with filename as one argument 448 "-l", 449 // remove some of the preprocessor flags, irrelevant for the AST 450 "-MT", "-MF", "-MD", "-MQ", "-MMD", "-MP", "-MG", "-E", "-cc1", "-S", "-M", "-MM", "-###", 451 ]) { 452 app.put(FilterClangFlag(f)); 453 } 454 // dfmt on 455 456 return app.data; 457 } 458 459 struct CompileCommandFilter { 460 FilterClangFlag[] filter; 461 int skipCompilerArgs = 0; 462 } 463 464 /// Parsed compiler flags. 465 struct ParseFlags { 466 /// The includes used in the compile command 467 static struct Include { 468 string payload; 469 alias payload this; 470 } 471 472 private { 473 bool forceSystemIncludes_; 474 } 475 476 /// The includes used in the compile command. 477 Include[] includes; 478 479 /// System include paths extracted from the compiler used for the file. 480 SystemIncludePath[] systemIncludes; 481 482 /// Specific flags for the file as parsed from the DB. 483 string[] cflags; 484 485 /// Compiler used to compile the item. 486 Compiler compiler; 487 488 void prependCflags(string[] v) { 489 this.cflags = v ~ this.cflags; 490 } 491 492 void appendCflags(string[] v) { 493 this.cflags ~= v; 494 } 495 496 /// Set to true to use -I instead of -isystem for system includes. 497 auto forceSystemIncludes(bool v) { 498 this.forceSystemIncludes_ = v; 499 return this; 500 } 501 502 bool hasSystemIncludes() @safe pure nothrow const @nogc { 503 return systemIncludes.length != 0; 504 } 505 506 string toString() @safe pure const { 507 import std.format : format; 508 509 return format("Compiler:%s flags: %-(%s %)", compiler, completeFlags); 510 } 511 512 /** Easy to use method that has the complete flags ready to use with a GCC 513 * complient compiler. 514 * 515 * This method assumes that -isystem is how to add system flags. 516 * 517 * Returns: flags with the system flags appended. 518 */ 519 string[] completeFlags() @safe pure nothrow const { 520 auto incl_param = forceSystemIncludes_ ? "-I" : "-isystem"; 521 522 return cflags.idup ~ systemIncludes.map!(a => [incl_param, a.value]).joiner.array; 523 } 524 525 alias completeFlags this; 526 527 this(Include[] incls, string[] flags) { 528 this(Compiler.init, incls, SystemIncludePath[].init, flags); 529 } 530 531 this(Compiler compiler, Include[] incls, string[] flags) { 532 this(compiler, incls, null, flags); 533 } 534 535 this(Compiler compiler, Include[] incls, SystemIncludePath[] sysincls, string[] flags) { 536 this.compiler = compiler; 537 this.includes = incls; 538 this.systemIncludes = sysincls; 539 this.cflags = flags; 540 } 541 } 542 543 /** Filter and normalize the compiler flags. 544 * 545 * - Sanitize the compiler command by removing flags matching the filter. 546 * - Remove excess white space. 547 * - Convert all filenames to absolute path. 548 */ 549 ParseFlags parseFlag(CompileCommand cmd, const CompileCommandFilter flag_filter) @safe { 550 import std.algorithm : among, strip, startsWith, count; 551 import std..string : empty, split; 552 553 static bool excludeStartWith(const string raw_flag, const FilterClangFlag[] flag_filter) @safe { 554 // the purpuse is to find if any of the flags in flag_filter matches 555 // the start of flag. 556 557 bool delegate(const FilterClangFlag) @safe cmp; 558 559 const parts = raw_flag.split('='); 560 if (parts.length == 2) { 561 // is a -foo=bar flag thus exact match is the only sensible 562 cmp = (const FilterClangFlag a) => raw_flag == a.payload; 563 } else { 564 // the flag has the argument merged thus have to check if the start match 565 cmp = (const FilterClangFlag a) => raw_flag.startsWith(a.payload); 566 } 567 568 // dfmt off 569 return 0 != flag_filter 570 .filter!(a => a.kind == FilterClangFlag.Kind.exclude) 571 // keep flags that are at least the length of values 572 .filter!(a => raw_flag.length >= a.length) 573 // if the flag is any of those in filter 574 .filter!cmp 575 .count(); 576 // dfmt on 577 } 578 579 static bool isQuotationMark(char c) @safe { 580 return c == '"'; 581 } 582 583 static bool isBackslash(char c) @safe { 584 return c == '\\'; 585 } 586 587 static bool isInclude(string flag) @safe { 588 return flag.length >= 2 && flag[0 .. 2] == "-I"; 589 } 590 591 static bool isCombinedIncludeFlag(string flag) @safe { 592 // if an include flag make it absolute, as one argument by checking 593 // length. 3 is to only match those that are -Ixyz 594 return flag.length >= 3 && isInclude(flag); 595 } 596 597 static bool isNotAFlag(string flag) @safe { 598 // good enough if it seem to be a file 599 return flag.length >= 1 && flag[0] != '-'; 600 } 601 602 /// Flags that take an argument that is a path that need to be transformed 603 /// to an absolute path. 604 static bool isFlagAndPath(string flag) @safe { 605 // list derived from clang --help 606 return 0 != flag.among("-I", "-idirafter", "-iframework", "-imacros", "-include-pch", 607 "-include", "-iquote", "-isysroot", "-isystem-after", "-isystem", "--sysroot"); 608 } 609 610 /// Flags that take an argument that is NOT a path. 611 static bool isFlagAndValue(string flag) @safe { 612 return 0 != flag.among("-D"); 613 } 614 615 /// Flags that are includes, but contains spaces, are wrapped in quotation marks (or slash). 616 static bool isIncludeWithQuotationMark(string flag) @safe { 617 // length is checked in isCombinedIncludeFlag 618 return isCombinedIncludeFlag(flag) && (isQuotationMark(flag[2]) || isBackslash(flag[2])); 619 } 620 621 /// Flags that are paths and contain spaces will start with a quotation mark (or slash). 622 static bool isStartingWithQuotationMark(string flag) @safe { 623 return !flag.empty && (isQuotationMark(flag[0]) || isBackslash(flag[0])); 624 } 625 626 /// When we know we are building a path that is space separated, 627 /// the last index of the last string should be a quotation mark. 628 static bool isEndingWithQuotationMark(string flag) @safe { 629 return !flag.empty && isQuotationMark(flag[$ - 1]); 630 } 631 632 static ParseFlags filterPair(string[] r, AbsolutePath workdir, 633 const FilterClangFlag[] flag_filter) @safe { 634 enum State { 635 /// keep the next flag IF none of the other transitions happens 636 keep, 637 /// forcefully keep the next argument as raw data 638 priorityKeepNextArg, 639 /// keep the next argument and transform to an absolute path 640 pathArgumentToAbsolute, 641 /// skip the next arg 642 skip, 643 /// skip the next arg, if it is not a flag 644 skipIfNotFlag, 645 /// use the next arg to create a complete path 646 checkingForEndQuotation, 647 } 648 649 auto st = State.keep; 650 auto rval = appender!(string[]); 651 auto includes = appender!(string[]); 652 auto compiler = Compiler(r.length == 0 ? null : r[0]); 653 auto path = appender!(char[])(); 654 655 string removeBackslashesAndQuotes(string arg) { 656 import std.conv : text; 657 import std.uni : byCodePoint, byGrapheme, Grapheme; 658 659 return arg.byGrapheme.filter!(a => !a.among(Grapheme('\\'), 660 Grapheme('"'))).byCodePoint.text; 661 } 662 663 void putNormalizedAbsolute(string arg) { 664 import std.path : buildNormalizedPath, absolutePath; 665 666 auto p = buildNormalizedPath(workdir, removeBackslashesAndQuotes(arg)).absolutePath; 667 rval.put(p); 668 includes.put(p); 669 } 670 671 foreach (arg; r) { 672 // First states and how to handle those. 673 // Then transitions from the state keep, which is the default state. 674 // 675 // The user controlled excludeStartWith must be before any other 676 // conditions after the states. It is to give the user the ability 677 // to filter out any flag. 678 679 if (st == State.skip) { 680 st = State.keep; 681 } else if (st == State.skipIfNotFlag && isNotAFlag(arg)) { 682 st = State.keep; 683 } else if (st == State.pathArgumentToAbsolute) { 684 if (isStartingWithQuotationMark(arg)) { 685 if (isEndingWithQuotationMark(arg)) { 686 st = State.keep; 687 putNormalizedAbsolute(arg); 688 } else { 689 st = State.checkingForEndQuotation; 690 path.put(arg); 691 } 692 } else { 693 st = State.keep; 694 putNormalizedAbsolute(arg); 695 } 696 } else if (st == State.priorityKeepNextArg) { 697 st = State.keep; 698 rval.put(arg); 699 } else if (st == State.checkingForEndQuotation) { 700 path.put(" "); 701 path.put(arg); 702 if (isEndingWithQuotationMark(arg)) { 703 // the end of a divided path 704 st = State.keep; 705 putNormalizedAbsolute(path.data.idup); 706 path.clear; 707 } 708 } else if (excludeStartWith(arg, flag_filter)) { 709 st = State.skipIfNotFlag; 710 } else if (isIncludeWithQuotationMark(arg)) { 711 rval.put("-I"); 712 if (arg.length >= 4) { 713 if (isEndingWithQuotationMark(arg)) { 714 // the path is wrapped in quotes (ex ['-I"path/to src"'] or ['-I\"path/to src\"']) 715 putNormalizedAbsolute(arg[2 .. $]); 716 } else { 717 // the path is divided (ex ['-I"path/to', 'src"'] or ['-I\"path/to', 'src\"']) 718 st = State.checkingForEndQuotation; 719 path.put(arg[2 .. $]); 720 } 721 } 722 } else if (isCombinedIncludeFlag(arg)) { 723 rval.put("-I"); 724 putNormalizedAbsolute(arg[2 .. $]); 725 } else if (isFlagAndPath(arg)) { 726 rval.put(arg); 727 st = State.pathArgumentToAbsolute; 728 } else if (isFlagAndValue(arg)) { 729 rval.put(arg); 730 st = State.priorityKeepNextArg; 731 } // parameter that seem to be filenames, remove 732 else if (isNotAFlag(arg)) { 733 // skipping 734 } else { 735 rval.put(arg); 736 } 737 } 738 return ParseFlags(compiler, includes.data.map!(a => ParseFlags.Include(a)).array, rval.data); 739 } 740 741 import std.algorithm : min; 742 743 string[] skipArgs = () @safe { 744 string[] args; 745 if (cmd.command.hasValue) 746 args = cmd.command.payload.dup; 747 if (args.length > flag_filter.skipCompilerArgs && flag_filter.skipCompilerArgs != 0) 748 args = args[min(flag_filter.skipCompilerArgs, args.length) .. $]; 749 return args; 750 }(); 751 752 auto pargs = filterPair(skipArgs, cmd.directory, flag_filter.filter); 753 754 return ParseFlags(pargs.compiler, pargs.includes, null, pargs.cflags); 755 } 756 757 /** Convert the string to a CompileCommandDB. 758 * 759 * Params: 760 * path = changes relative paths to be relative this parameter 761 * data = input to convert 762 */ 763 CompileCommandDB toCompileCommandDB(string data, Path path) @safe { 764 auto app = appender!(CompileCommand[])(); 765 data.parseCommands(CompileDbFile(cast(string) path), app); 766 return CompileCommandDB(app.data); 767 } 768 769 CompileCommandDB fromArgCompileDb(AbsolutePath[] paths) @safe { 770 return fromArgCompileDb(paths.map!(a => cast(string) a).array); 771 } 772 773 /// Import and merge many compilation databases into one DB. 774 CompileCommandDB fromArgCompileDb(string[] paths) @safe { 775 auto app = appender!(CompileCommand[])(); 776 paths.orDefaultDb.fromFiles(app); 777 778 return CompileCommandDB(app.data); 779 } 780 781 /// Flags to exclude from the flags passed on to the clang parser. 782 struct FilterClangFlag { 783 string payload; 784 alias payload this; 785 786 enum Kind { 787 exclude 788 } 789 790 Kind kind; 791 } 792 793 @("Should be cflags with all unnecessary flags removed") 794 unittest { 795 auto cmd = toCompileCommand("/home", "file1.cpp", [ 796 "g++", "-MD", "-lfoo.a", "-l", "bar.a", "-I", "bar", "-Igun", "-c", 797 "a_filename.c" 798 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 799 auto s = cmd.get.parseFlag(defaultCompilerFilter); 800 s.cflags.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]); 801 s.includes.shouldEqual(["/home/bar", "/home/gun"]); 802 } 803 804 @("Should be cflags with some excess spacing") 805 unittest { 806 auto cmd = toCompileCommand("/home", "file1.cpp", [ 807 "g++", "-MD", "-lfoo.a", "-l", "bar.a", "-I", "bar", "-Igun" 808 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 809 810 auto s = cmd.get.parseFlag(defaultCompilerFilter); 811 s.cflags.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]); 812 s.includes.shouldEqual(["/home/bar", "/home/gun"]); 813 } 814 815 @("Should be cflags with machine dependent removed") 816 unittest { 817 auto cmd = toCompileCommand("/home", "file1.cpp", [ 818 "g++", "-mfoo", "-m", "bar", "-MD", "-lfoo.a", "-l", "bar.a", "-I", 819 "bar", "-Igun", "-c", "a_filename.c" 820 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 821 822 auto s = cmd.get.parseFlag(defaultCompilerFilter); 823 s.cflags.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]); 824 s.includes.shouldEqual(["/home/bar", "/home/gun"]); 825 } 826 827 @("Should be cflags with all -f removed") 828 unittest { 829 auto cmd = toCompileCommand("/home", "file1.cpp", [ 830 "g++", "-fmany-fooo", "-I", "bar", "-fno-fooo", "-Igun", "-flolol", 831 "-c", "a_filename.c" 832 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 833 834 auto s = cmd.get.parseFlag(defaultCompilerFilter); 835 s.cflags.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]); 836 s.includes.shouldEqual(["/home/bar", "/home/gun"]); 837 } 838 839 @("shall NOT remove -std=xyz flags") 840 unittest { 841 auto cmd = toCompileCommand("/home", "file1.cpp", [ 842 "g++", "-std=c++11", "-c", "a_filename.c" 843 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 844 845 auto s = cmd.get.parseFlag(defaultCompilerFilter); 846 s.cflags.shouldEqual(["-std=c++11"]); 847 } 848 849 @("shall remove -mfloat-gprs=double") 850 unittest { 851 auto cmd = toCompileCommand("/home", "file1.cpp", [ 852 "g++", "-std=c++11", "-mfloat-gprs=double", "-c", "a_filename.c" 853 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 854 auto my_filter = CompileCommandFilter(defaultCompilerFlagFilter, 0); 855 my_filter.filter ~= FilterClangFlag("-mfloat-gprs=double", FilterClangFlag.Kind.exclude); 856 auto s = cmd.get.parseFlag(my_filter); 857 s.cflags.shouldEqual(["-std=c++11"]); 858 } 859 860 @("Shall keep all compiler flags as they are") 861 unittest { 862 auto cmd = toCompileCommand("/home", "file1.cpp", ["g++", "-Da", "-D", 863 "b"], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 864 865 auto s = cmd.get.parseFlag(defaultCompilerFilter); 866 s.cflags.shouldEqual(["-Da", "-D", "b"]); 867 } 868 869 version (unittest) { 870 import std.file : getcwd; 871 import std.path : absolutePath; 872 import std.format : format; 873 874 // contains a bit of extra junk that is expected to be removed 875 immutable string dummy_path = "/path/to/../to/./db/compilation_db.json"; 876 immutable string dummy_dir = "/path/to/db"; 877 878 enum raw_dummy1 = `[ 879 { 880 "directory": "dir1/dir2", 881 "command": "g++ -Idir1 -c -o binary file1.cpp", 882 "file": "file1.cpp" 883 } 884 ]`; 885 886 enum raw_dummy2 = `[ 887 { 888 "directory": "dir", 889 "command": "g++ -Idir1 -c -o binary file1.cpp", 890 "file": "file1.cpp" 891 }, 892 { 893 "directory": "dir", 894 "command": "g++ -Idir1 -c -o binary file2.cpp", 895 "file": "file2.cpp" 896 } 897 ]`; 898 899 enum raw_dummy3 = `[ 900 { 901 "directory": "dir1", 902 "command": "g++ -Idir1 -c -o binary file3.cpp", 903 "file": "file3.cpp" 904 }, 905 { 906 "directory": "dir2", 907 "command": "g++ -Idir1 -c -o binary file3.cpp", 908 "file": "file3.cpp" 909 } 910 ]`; 911 912 enum raw_dummy4 = `[ 913 { 914 "directory": "dir1", 915 "arguments": "g++ -Idir1 -c -o binary file3.cpp", 916 "file": "file3.cpp", 917 "output": "file3.o" 918 }, 919 { 920 "directory": "dir2", 921 "arguments": "g++ -Idir1 -c -o binary file3.cpp", 922 "file": "file3.cpp", 923 "output": "file3.o" 924 } 925 ]`; 926 927 enum raw_dummy5 = `[ 928 { 929 "directory": "dir1", 930 "arguments": ["g++", "-Idir1", "-c", "-o", "binary", "file3.cpp"], 931 "file": "file3.cpp", 932 "output": "file3.o" 933 }, 934 { 935 "directory": "dir2", 936 "arguments": ["g++", "-Idir1", "-c", "-o", "binary", "file3.cpp"], 937 "file": "file3.cpp", 938 "output": "file3.o" 939 } 940 ]`; 941 } 942 943 @("Should be a compile command DB") 944 unittest { 945 auto app = appender!(CompileCommand[])(); 946 raw_dummy1.parseCommands(CompileDbFile(dummy_path), app); 947 auto cmds = app.data; 948 949 assert(cmds.length == 1); 950 (cast(string) cmds[0].directory).shouldEqual(dummy_dir ~ "/dir1/dir2"); 951 cmds[0].command.shouldEqual([ 952 "g++", "-Idir1", "-c", "-o", "binary", "file1.cpp" 953 ]); 954 (cast(string) cmds[0].file).shouldEqual("file1.cpp"); 955 (cast(string) cmds[0].absoluteFile).shouldEqual(dummy_dir ~ "/dir1/dir2/file1.cpp"); 956 } 957 958 @("Should be a DB with two entries") 959 unittest { 960 auto app = appender!(CompileCommand[])(); 961 raw_dummy2.parseCommands(CompileDbFile(dummy_path), app); 962 auto cmds = app.data; 963 964 (cast(string) cmds[0].file).shouldEqual("file1.cpp"); 965 (cast(string) cmds[1].file).shouldEqual("file2.cpp"); 966 } 967 968 @("Should find filename") 969 unittest { 970 auto app = appender!(CompileCommand[])(); 971 raw_dummy2.parseCommands(CompileDbFile(dummy_path), app); 972 auto cmds = CompileCommandDB(app.data); 973 974 auto found = cmds.find(dummy_dir ~ "/dir/file2.cpp"); 975 assert(found.length == 1); 976 (cast(string) found[0].file).shouldEqual("file2.cpp"); 977 } 978 979 @("Should find no match by using an absolute path that doesn't exist in DB") 980 unittest { 981 auto app = appender!(CompileCommand[])(); 982 raw_dummy2.parseCommands(CompileDbFile(dummy_path), app); 983 auto cmds = CompileCommandDB(app.data); 984 985 auto found = cmds.find("./file2.cpp"); 986 assert(found.length == 0); 987 } 988 989 @("Should find one match by using the absolute filename to disambiguous") 990 unittest { 991 auto app = appender!(CompileCommand[])(); 992 raw_dummy3.parseCommands(CompileDbFile(dummy_path), app); 993 auto cmds = CompileCommandDB(app.data); 994 995 auto found = cmds.find(dummy_dir ~ "/dir2/file3.cpp"); 996 assert(found.length == 1); 997 998 found.toString.shouldEqual(format("%s/dir2 999 file3.cpp 1000 %s/dir2/file3.cpp 1001 g++ -Idir1 -c -o binary file3.cpp 1002 ", dummy_dir, dummy_dir)); 1003 } 1004 1005 @("Should be a pretty printed search result") 1006 unittest { 1007 auto app = appender!(CompileCommand[])(); 1008 raw_dummy2.parseCommands(CompileDbFile(dummy_path), app); 1009 auto cmds = CompileCommandDB(app.data); 1010 auto found = cmds.find(dummy_dir ~ "/dir/file2.cpp"); 1011 1012 found.toString.shouldEqual(format("%s/dir 1013 file2.cpp 1014 %s/dir/file2.cpp 1015 g++ -Idir1 -c -o binary file2.cpp 1016 ", dummy_dir, dummy_dir)); 1017 } 1018 1019 @("Should be a compile command DB with relative path") 1020 unittest { 1021 enum raw = `[ 1022 { 1023 "directory": ".", 1024 "command": "g++ -Idir1 -c -o binary file1.cpp", 1025 "file": "file1.cpp" 1026 } 1027 ]`; 1028 auto app = appender!(CompileCommand[])(); 1029 raw.parseCommands(CompileDbFile(dummy_path), app); 1030 auto cmds = app.data; 1031 1032 assert(cmds.length == 1); 1033 (cast(string) cmds[0].directory).shouldEqual(dummy_dir); 1034 (cast(string) cmds[0].file).shouldEqual("file1.cpp"); 1035 (cast(string) cmds[0].absoluteFile).shouldEqual(dummy_dir ~ "/file1.cpp"); 1036 } 1037 1038 @("Should be a DB read from a relative path with the contained paths adjusted appropriately") 1039 unittest { 1040 auto app = appender!(CompileCommand[])(); 1041 raw_dummy3.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1042 auto cmds = CompileCommandDB(app.data); 1043 1044 // trusted: constructing a path in memory which is never used for writing. 1045 auto abs_path = () @trusted { return getcwd() ~ "/path"; }(); 1046 1047 auto found = cmds.find(abs_path ~ "/dir2/file3.cpp"); 1048 assert(found.length == 1); 1049 1050 found.toString.shouldEqual(format("%s/dir2 1051 file3.cpp 1052 %s/dir2/file3.cpp 1053 g++ -Idir1 -c -o binary file3.cpp 1054 ", abs_path, abs_path)); 1055 } 1056 1057 @("shall extract arguments, file, directory and output with absolute paths") 1058 unittest { 1059 auto app = appender!(CompileCommand[])(); 1060 raw_dummy4.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1061 auto cmds = CompileCommandDB(app.data); 1062 1063 // trusted: constructing a path in memory which is never used for writing. 1064 auto abs_path = () @trusted { return getcwd() ~ "/path"; }(); 1065 1066 auto found = cmds.find(buildPath(abs_path, "dir2", "file3.cpp")); 1067 assert(found.length == 1); 1068 1069 found.toString.shouldEqual(format("%s/dir2 1070 file3.cpp 1071 %s/dir2/file3.cpp 1072 file3.o 1073 %s/dir2/file3.o 1074 g++ -Idir1 -c -o binary file3.cpp 1075 ", abs_path, abs_path, abs_path)); 1076 } 1077 1078 @("shall be the compiler flags derived from the arguments attribute") 1079 unittest { 1080 auto app = appender!(CompileCommand[])(); 1081 raw_dummy4.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1082 auto cmds = CompileCommandDB(app.data); 1083 1084 // trusted: constructing a path in memory which is never used for writing. 1085 auto abs_path = () @trusted { return getcwd() ~ "/path"; }(); 1086 1087 auto found = cmds.find(buildPath(abs_path, "dir2", "file3.cpp")); 1088 assert(found.length == 1); 1089 1090 found[0].parseFlag(defaultCompilerFilter).cflags.shouldEqual([ 1091 "-I", buildPath(abs_path, "dir2", "dir1") 1092 ]); 1093 } 1094 1095 @("shall find the entry based on an output match") 1096 unittest { 1097 auto app = appender!(CompileCommand[])(); 1098 raw_dummy4.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1099 auto cmds = CompileCommandDB(app.data); 1100 1101 // trusted: constructing a path in memory which is never used for writing. 1102 auto abs_path = () @trusted { return getcwd() ~ "/path"; }(); 1103 1104 auto found = cmds.find(buildPath(abs_path, "dir2", "file3.o")); 1105 assert(found.length == 1); 1106 1107 (cast(string) found[0].absoluteFile).shouldEqual(buildPath(abs_path, "dir2", "file3.cpp")); 1108 } 1109 1110 @("shall parse the compilation database when *arguments* is a json list") 1111 unittest { 1112 auto app = appender!(CompileCommand[])(); 1113 raw_dummy5.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1114 auto cmds = CompileCommandDB(app.data); 1115 1116 // trusted: constructing a path in memory which is never used for writing. 1117 auto abs_path = () @trusted { return getcwd() ~ "/path"; }(); 1118 1119 auto found = cmds.find(buildPath(abs_path, "dir2", "file3.o")); 1120 assert(found.length == 1); 1121 1122 (cast(string) found[0].absoluteFile).shouldEqual(buildPath(abs_path, "dir2", "file3.cpp")); 1123 } 1124 1125 @("shall parse the compilation database and find a match via the glob pattern") 1126 unittest { 1127 import std.path : baseName; 1128 1129 auto app = appender!(CompileCommand[])(); 1130 raw_dummy5.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1131 auto cmds = CompileCommandDB(app.data); 1132 1133 auto found = cmds.find("*/dir2/file3.cpp"); 1134 assert(found.length == 1); 1135 1136 found[0].absoluteFile.baseName.shouldEqual("file3.cpp"); 1137 } 1138 1139 @("shall extract filepath from includes correctly when there is spaces in the path") 1140 unittest { 1141 auto cmd = toCompileCommand("/home", "file.cpp", [ 1142 "-I", `"dir with spaces"`, "-I", `\"dir with spaces\"` 1143 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 1144 auto pargs = cmd.get.parseFlag(defaultCompilerFilter); 1145 pargs.cflags.shouldEqual([ 1146 "-I", "/home/dir with spaces", "-I", "/home/dir with spaces" 1147 ]); 1148 pargs.includes.shouldEqual([ 1149 "/home/dir with spaces", "/home/dir with spaces" 1150 ]); 1151 } 1152 1153 @("shall handle path with spaces, both as separate string and combined with backslash") 1154 unittest { 1155 auto cmd = toCompileCommand("/project", "file.cpp", [ 1156 "-I", `"separate dir/with space"`, "-I", `\"separate dir/with space\"`, 1157 `-I"combined dir/with space"`, `-I\"combined dir/with space\"`, 1158 ], AbsoluteCompileDbDirectory("/project".Path.AbsolutePath), null); 1159 auto pargs = cmd.get.parseFlag(defaultCompilerFilter); 1160 pargs.cflags.shouldEqual([ 1161 "-I", "/project/separate dir/with space", "-I", 1162 "/project/separate dir/with space", "-I", 1163 "/project/combined dir/with space", "-I", 1164 "/project/combined dir/with space" 1165 ]); 1166 pargs.includes.shouldEqual([ 1167 "/project/separate dir/with space", "/project/separate dir/with space", 1168 "/project/combined dir/with space", "/project/combined dir/with space" 1169 ]); 1170 } 1171 1172 @("shall handle path with consecutive spaces") 1173 unittest { 1174 auto cmd = toCompileCommand("/project", "file.cpp", 1175 [ 1176 `-I"one space/lots of space"`, 1177 `-I\"one space/lots of space\"`, `-I`, 1178 `"one space/lots of space"`, `-I`, 1179 `\"one space/lots of space\"`, 1180 ], AbsoluteCompileDbDirectory("/project".Path.AbsolutePath), null); 1181 auto pargs = cmd.get.parseFlag(defaultCompilerFilter); 1182 pargs.cflags.shouldEqual([ 1183 "-I", "/project/one space/lots of space", "-I", 1184 "/project/one space/lots of space", "-I", 1185 "/project/one space/lots of space", "-I", 1186 "/project/one space/lots of space", 1187 ]); 1188 pargs.includes.shouldEqual([ 1189 "/project/one space/lots of space", 1190 "/project/one space/lots of space", 1191 "/project/one space/lots of space", 1192 "/project/one space/lots of space" 1193 ]); 1194 }